/*
 * Created on 06-Mar-2005
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 */

package org.gudy.azureus2.core3.util.protocol.magnet;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import org.gudy.azureus2.core3.util.AESemaphore;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;
import org.gudy.azureus2.core3.util.TimerEventPeriodic;

/**
 * @author parg
 * 
 */

public class MagnetConnection2 extends HttpURLConnection {
    private static final String NL = "\r\n";

    private static LinkedList<MagnetOutputStream> active_os = new LinkedList<MagnetOutputStream>();
    private static TimerEventPeriodic active_os_event;

    private static void addActiveStream(MagnetOutputStream os) {
        synchronized (active_os) {

            active_os.add(os);

            if (active_os.size() == 1 && active_os_event == null) {

                active_os_event = SimpleTimer.addPeriodicEvent("mos:checker", 30 * 1000, new TimerEventPerformer() {
                    public void perform(TimerEvent event) {
                        List<MagnetOutputStream> active;

                        synchronized (active_os) {

                            active = new ArrayList<MagnetOutputStream>(active_os);
                        }

                        for (MagnetOutputStream os : active) {

                            os.timerCheck();
                        }
                    }
                });
            }
        }
    }

    private static void removeActiveStream(MagnetOutputStream os) {
        synchronized (active_os) {

            active_os.remove(os);

            if (active_os.size() == 0 && active_os_event != null) {

                active_os_event.cancel();

                active_os_event = null;
            }
        }
    }

    private MagnetHandler handler;
    private OutputStream output_stream;
    private InputStream input_stream;

    private LinkedList<String> status_list = new LinkedList<String>();

    public MagnetConnection2(URL _url, MagnetHandler _handler) {
        super(_url);

        handler = _handler;
    }

    public void connect()

    throws IOException {
        MagnetOutputStream mos = new MagnetOutputStream();
        MagnetInputStream mis = new MagnetInputStream(mos);

        input_stream = mis;
        output_stream = mos;

        handler.process(getURL(), mos);
    }

    public InputStream getInputStream()

    throws IOException {
        String line = "";

        byte[] buffer = new byte[1];

        byte[] line_bytes = new byte[2048];
        int line_bytes_pos = 0;

        while (true) {

            int len = input_stream.read(buffer);

            if (len == -1) {

                break;
            }

            line += (char) buffer[0];

            line_bytes[line_bytes_pos++] = buffer[0];

            if (line.endsWith(NL)) {

                line = line.trim();

                if (line.length() == 0) {

                    break;
                }

                if (line.startsWith("X-Report:")) {

                    line = new String(line_bytes, 0, line_bytes_pos, "UTF-8");

                    line = line.substring(9);

                    line = line.trim();

                    synchronized (status_list) {

                        String str = Character.toUpperCase(line.charAt(0)) + line.substring(1);

                        if (status_list.size() == 0) {

                            status_list.addLast(str);

                        } else if (!status_list.getLast().equals(str)) {

                            status_list.addLast(str);
                        }
                    }
                }

                line = "";
                line_bytes_pos = 0;
            }
        }

        return (input_stream);
    }

    public int getResponseCode() {
        return (HTTP_OK);
    }

    public String getResponseMessage() {
        synchronized (status_list) {

            if (status_list.size() == 0) {

                return ("");

            } else if (status_list.size() == 1) {

                return (status_list.get(0));

            } else {

                return (status_list.removeFirst());
            }
        }
    }

    public List<String> getResponseMessages(boolean error_only) {
        synchronized (status_list) {

            if (error_only) {

                List<String> response = new ArrayList<String>();

                for (String s : status_list) {

                    if (s.toLowerCase().startsWith("error:")) {

                        response.add(s);
                    }
                }

                return (response);

            } else {

                return (new ArrayList<String>(status_list));
            }
        }
    }

    public boolean usingProxy() {
        return (false);
    }

    public void disconnect() {
        try {
            output_stream.close();

        } catch (Throwable e) {

            Debug.printStackTrace(e);
        }

        try {
            input_stream.close();

        } catch (Throwable e) {

            Debug.printStackTrace(e);
        }
    }

    private class MagnetOutputStream extends OutputStream {
        private LinkedList<byte[]> buffers = new LinkedList<byte[]>();
        private int available;
        private AESemaphore buffer_sem = new AESemaphore("mos:buffers");
        private boolean closed;

        private long last_read = SystemTime.getMonotonousTime();
        private int read_active;

        private MagnetOutputStream() {
            addActiveStream(this);
        }

        private void timerCheck() {
            synchronized (buffers) {

                if (closed || read_active > 0 || SystemTime.getMonotonousTime() - last_read < 60 * 1000) {

                    return;
                }
            }

            Debug.out("Abandoning magnet download for " + MagnetConnection2.this.getURL() + " as no active reader");

            try {
                close();

            } catch (Throwable e) {

            }
        }

        public void write(int b)

        throws IOException {
            synchronized (buffers) {

                if (closed) {

                    throw (new IOException("Connection closed"));
                }

                buffers.addLast(new byte[] { (byte) b });

                available++;

                buffer_sem.release();
            }
        }

        public void write(byte b[], int off, int len)

        throws IOException {
            synchronized (buffers) {

                if (closed) {

                    throw (new IOException("Connection closed"));
                }

                if (len > 0) {

                    byte[] new_b = new byte[len];

                    System.arraycopy(b, off, new_b, 0, len);

                    buffers.addLast(new_b);

                    available += len;

                    buffer_sem.release();
                }
            }
        }

        private int read()

        throws IOException {
            synchronized (buffers) {

                last_read = SystemTime.getMonotonousTime();

                read_active++;
            }

            try {
                buffer_sem.reserve();

            } finally {

                synchronized (buffers) {

                    last_read = SystemTime.getMonotonousTime();

                    read_active--;
                }
            }

            synchronized (buffers) {

                if (closed && buffers.size() == 0) {

                    return (-1);
                }

                byte[] b = buffers.removeFirst();

                if (b.length > 1) {

                    for (int i = b.length - 1; i > 0; i--) {

                        buffers.addFirst(new byte[] { b[i] });

                        buffer_sem.release();
                    }
                }

                available--;

                return (((int) b[0]) & 0x000000ff);
            }
        }

        private int read(byte buffer[], int off, int len)

        throws IOException {
            synchronized (buffers) {

                last_read = SystemTime.getMonotonousTime();

                read_active++;
            }

            try {
                buffer_sem.reserve();

            } finally {

                synchronized (buffers) {

                    last_read = SystemTime.getMonotonousTime();

                    read_active--;
                }
            }

            synchronized (buffers) {

                int read = 0;

                while (true) {

                    if (closed && buffers.size() == 0) {

                        return (read == 0 ? -1 : read);
                    }

                    byte[] b = buffers.removeFirst();

                    int b_len = b.length;

                    if (b_len >= len) {

                        read += len;

                        System.arraycopy(b, 0, buffer, off, len);

                        if (b_len > len) {

                            byte[] new_b = new byte[b_len - len];

                            System.arraycopy(b, len, new_b, 0, new_b.length);

                            buffers.addFirst(new_b);

                            buffer_sem.release();
                        }

                        break;

                    } else {

                        read += b_len;

                        System.arraycopy(b, 0, buffer, off, b_len);

                        off += b_len;
                        len -= b_len;
                    }

                    if (!buffer_sem.reserveIfAvailable()) {

                        break;
                    }
                }

                available -= read;

                return (read);
            }
        }

        private int available()

        throws IOException {
            synchronized (buffers) {

                if (closed) {

                    throw (new IOException("Connection closed"));
                }

                return (available);
            }
        }

        public void close()

        throws IOException {
            synchronized (buffers) {

                if (closed) {

                    return;
                }

                closed = true;

                buffer_sem.releaseForever();
            }

            removeActiveStream(this);
        }
    }

    private class MagnetInputStream extends InputStream {
        private MagnetOutputStream out;

        private MagnetInputStream(MagnetOutputStream _out) {
            out = _out;
        }

        public int read()

        throws IOException {
            return (out.read());
        }

        public int read(byte b[], int off, int len)

        throws IOException {
            return (out.read(b, off, len));
        }

        public int available()

        throws IOException {
            return (out.available());
        }

        public long skip(long n)

        throws IOException {
            throw (new IOException("Not supported"));
        }

        public void close() throws IOException {
            out.close();
        }
    }

    public interface MagnetHandler {
        public void process(URL magnet, OutputStream os)

        throws IOException;
    }
}
